Skip to content

direct: store a dashboard contents digest in state instead of the contents - #6105

Open
Sankalp-Mittal wants to merge 40 commits into
mainfrom
sankalp-mittal/dashboards-sha-state
Open

direct: store a dashboard contents digest in state instead of the contents#6105
Sankalp-Mittal wants to merge 40 commits into
mainfrom
sankalp-mittal/dashboards-sha-state

Conversation

@Sankalp-Mittal

@Sankalp-Mittal Sankalp-Mittal commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Changes

Add a hashed_in_state lifecycle rule to the direct engine. A field declared under it is
persisted to state as a sha256_hashed_in_state:<hex> digest of its contents instead of the
contents themselves. dashboards.serialized_dashboard is the first (and only) field to
declare it.

CompactState applies the rule, and it runs on every value entering the state diff — the
saved state, the local config and the remapped remote — as well as on the state being
persisted. Once the saved value is a digest, all three sides must be digests or the
comparisons would be hash-vs-content nonsense. The full contents stay in the plan's
new_state and are sent to the API on every create and update, so deploys are unaffected.

Hashing is skipped when it would not pay for itself: a value whose JSON encoding is no longer
than the 87-byte placeholder is persisted raw. The verdict depends only on the value, so all
call sites agree on it and the diff sides stay comparable — including when a field is small
for one resource and large for another, or grows past the threshold between deploys.

Declaring a field is restricted to top-level paths and rejected otherwise: CompactState
shallow-copies the state so the caller's value (reused for the deploy API call) is untouched,
which only isolates depth-1 fields.

Why

serialized_dashboard holds inlined dashboard JSON, frequently multi-MB, and state only ever
compares it for equality — nothing reads the stored copy back out. A digest answers the same
question, so resources.json no longer carries a copy of every dashboard on every deploy.

No state version bump: legacy state holding the full contents is hashed on read for
comparison and rewritten compactly on the next save, so existing bundles do not report a
dashboard update on upgrade. bundle plan still shows the real content diff.

hashed_in_state is orthogonal to ignore_remote_changes. serialized_dashboard happens to
need both, for unrelated reasons: it is hashed because the JSON is large, and separately
ignored remotely because the server normalizes it so its remote digest never equals the config
digest — drift is detected via etag.

Tests

resources/dashboard-state-sha asserts state holds only the digest while the API receives the
full contents, across create, a no-op re-plan, an edit and the resulting update, for READPLAN
in ["", "1"]. Its fixture is sized above the threshold on purpose.

Unit tests cover digest stability and idempotency, the top-level-path restriction, the
legacy-full-content upgrade path, and both sides of the size threshold. The fixtures are
guarded by requireLargeEnoughToHash / requireTooSmallToHash, so resizing one fails with an
actionable message rather than silently inverting what the test proves.

Dashboard, bind and migrate goldens regenerated; the digest is masked as [HASH] by a single
rule in acceptance/bundle/test.toml, anchored on the prefix.

This pull request and its description were written by Isaac.

@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Approval status: pending

/acceptance/bundle/ - needs approval

18 files changed
Suggested: @denik
Also eligible: @pietern, @janniklasrose, @andrewnester, @shreyas-goenka, @anton-107, @lennartkats-db

/bundle/ - needs approval

10 files changed
Suggested: @denik
Also eligible: @pietern, @janniklasrose, @andrewnester, @shreyas-goenka, @anton-107, @lennartkats-db

General files (require maintainer)

Files: libs/cache/fingerprint_test.go
Based on git history:

  • @denik -- recent work in bundle/direct/, bundle/direct/dresources/, bundle/migrate/

Any maintainer (@andrewnester, @anton-107, @denik, @pietern, @shreyas-goenka, @simonfaltum, @renaudhartert-db, @janniklasrose, @lennartkats-db) can approve all areas.
See OWNERS for ownership rules.

@eng-dev-ecosystem-bot

eng-dev-ecosystem-bot commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Integration test report

Commit: 823ae7d

Run: 31103690545

Env 🔄​flaky 💚​RECOVERED 🙈​SKIP ✅​pass 🙈​skip Time
🔄​ aws linux 3 3 4 302 1108 11:19
💚​ aws windows 4 4 306 1106 9:12
💚​ azure linux 4 4 303 1108 8:42
💚​ azure windows 4 4 305 1106 9:56
💚​ gcp linux 1 5 304 1108 7:58
💚​ gcp windows 1 5 306 1106 10:48
10 interesting tests: 4 SKIP, 3 flaky, 3 RECOVERED
Test Name aws linux aws windows azure linux azure windows gcp linux gcp windows
🔄​ TestAccept 🔄​f 💚​R 💚​R 💚​R 💚​R 💚​R
🙈​ TestAccept/bundle/invariant/no_drift 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🔄​ TestAccept/bundle/resources/dashboards/detect-change 🔄​f ✅​p ✅​p ✅​p ✅​p ✅​p
🔄​ TestAccept/bundle/resources/dashboards/detect-change/DATABRICKS_BUNDLE_ENGINE=direct 🔄​f ✅​p ✅​p ✅​p ✅​p ✅​p
🙈​ TestAccept/bundle/resources/vector_search_endpoints/drift/recreated_same_name 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/bundle/resources/vector_search_indexes/recreate/embedding_dimension 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/ssh/connection 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
💚​ TestFetchRepositoryInfoAPI_FromRepo 💚​R 💚​R 💚​R 💚​R 🙈​S 🙈​S
💚​ TestFetchRepositoryInfoAPI_FromRepo/root 💚​R 💚​R 💚​R 💚​R
💚​ TestFetchRepositoryInfoAPI_FromRepo/subdir 💚​R 💚​R 💚​R 💚​R
Top 25 slowest tests (at least 2 minutes):
duration env testname
5:14 gcp windows TestAccept
4:48 aws windows TestAccept/bundle/deployment/bind/dashboard/recreation/DATABRICKS_BUNDLE_ENGINE=terraform/READPLAN=
3:46 azure windows TestAccept/bundle/resources/dashboards/unpublish-out-of-band/DATABRICKS_BUNDLE_ENGINE=direct
3:35 azure windows TestAccept
3:15 aws windows TestAccept
3:14 azure windows TestAccept/bundle/resources/dashboards/unpublish-out-of-band/DATABRICKS_BUNDLE_ENGINE=terraform
3:04 gcp windows TestAccept/bundle/deployment/bind/dashboard/recreation/DATABRICKS_BUNDLE_ENGINE=direct/READPLAN=
2:50 gcp windows TestAccept/bundle/deployment/bind/dashboard/recreation/DATABRICKS_BUNDLE_ENGINE=terraform/READPLAN=
2:48 aws windows TestAccept/bundle/resources/dashboards/detect-change/DATABRICKS_BUNDLE_ENGINE=terraform
2:46 gcp linux TestAccept/bundle/deployment/bind/dashboard/recreation/DATABRICKS_BUNDLE_ENGINE=direct/READPLAN=
2:46 aws windows TestAccept/bundle/resources/dashboards/simple/DATABRICKS_BUNDLE_ENGINE=terraform
2:30 aws windows TestAccept/bundle/resources/dashboards/unpublish-out-of-band/DATABRICKS_BUNDLE_ENGINE=terraform
2:27 azure windows TestAccept/bundle/resources/dashboards/detect-change/DATABRICKS_BUNDLE_ENGINE=direct
2:27 aws windows TestAccept/bundle/deployment/bind/dashboard/recreation/DATABRICKS_BUNDLE_ENGINE=direct/READPLAN=
2:27 aws linux TestAccept/bundle/resources/dashboards/unpublish-out-of-band/DATABRICKS_BUNDLE_ENGINE=terraform
2:26 azure windows TestAccept/bundle/resources/dashboards/detect-change/DATABRICKS_BUNDLE_ENGINE=terraform
2:19 gcp windows TestAccept/bundle/deployment/bind/dashboard/recreation/DATABRICKS_BUNDLE_ENGINE=direct/READPLAN=1
2:18 gcp windows TestImportDirDoesNotOverwrite
2:17 gcp windows TestAccept/bundle/resources/dashboards/detect-change/DATABRICKS_BUNDLE_ENGINE=direct
2:16 azure windows TestAccept/bundle/deployment/bind/dashboard/recreation/DATABRICKS_BUNDLE_ENGINE=direct/READPLAN=
2:11 azure linux TestAccept/bundle/deployment/bind/dashboard/recreation/DATABRICKS_BUNDLE_ENGINE=terraform/READPLAN=
2:10 azure windows TestAccept/bundle/deployment/bind/dashboard/recreation/DATABRICKS_BUNDLE_ENGINE=terraform/READPLAN=
2:03 gcp windows TestAccept/bundle/resources/dashboards/detect-change/DATABRICKS_BUNDLE_ENGINE=terraform
2:01 gcp linux TestAccept/bundle/resources/dashboards/detect-change/DATABRICKS_BUNDLE_ENGINE=terraform
2:00 gcp windows TestAccept/bundle/resources/dashboards/unpublish-out-of-band/DATABRICKS_BUNDLE_ENGINE=direct

@Sankalp-Mittal
Sankalp-Mittal marked this pull request as ready for review July 30, 2026 15:55
Sankalp-Mittal and others added 5 commits July 31, 2026 07:29
Empty commit to re-run the pipeline; the previous integration run failed only
on gcp-linux TestFsCp* tests, which this PR does not touch (unrelated infra
flake).

Co-authored-by: Isaac

@denik denik left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we automatically migrate old state so that there is no drift when we load state with full serialized dashboard?

It would be nice to have acc test for this case.

"parent_path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle-[UNIQUE_NAME]/default/resources",
"published": true,
"serialized_dashboard": "{\"pages\":[{\"displayName\":\"Page One\",\"name\":\"02724bf2\"}]}",
"serialized_dashboard": "sha256_hashed_in_state:[HASH]",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"sha256_hashed_in_state:[HASH]"

could just be sha256:[HASH]

Also, do we need a replacement for HASH here, dopes it change?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • The long name was to avoid potential future conflicts in naming as right now this prefix is used to check if a field has been hashed or not
  • Added the replacement for [HASH] since makes the output much more readable.

Comment thread libs/hash/hash.go Outdated

@shreyas-goenka shreyas-goenka left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Looks good to me mostly. Can you see whether we can avoid this showing up in the plan diff? If that's a big lift or not?

         "old": "sha256_hashed_in_state:[HASH][0]",
          "new": "sha256_hashed_in_state:[HASH][0]",
          "remote": "sha256_hashed_in_state:[HASH][1]"

// stateHashPrefix marks a state value that holds a content hash instead of the
// raw value. Since this is part of the on-disk state format, changing it is not
// backwards compatible.
const stateHashPrefix = "sha256_hashed_in_state:"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the prefix can just be

Suggested change
const stateHashPrefix = "sha256_hashed_in_state:"
const stateHashPrefix = "sha256"

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The long name was to avoid potential future conflicts in naming as right now this prefix is used to check if a field has been hashed or not (a proxy for checking if the data is stored according to this new format)


// TestHashStateValueIdempotent verifies re-hashing an existing placeholder returns it
// unchanged, so re-compacting an already-compact state does not double-hash.
func TestHashStateValueIdempotent(t *testing.T) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test can be removed. We already have coverage above.

"old": "{\"pages\":[{\"displayName\":\"Test Dashboard\",\"name\":\"test-page\"}]}",
"new": "{\"pages\":[{\"displayName\":\"Test Dashboard\",\"name\":\"test-page\"}]}",
"remote": "{\"pages\":[{\"displayName\":\"Test Dashboard\",\"name\":\"test-page\",\"pageType\":\"PAGE_TYPE_CANVAS\"}]}"
"old": "sha256_hashed_in_state:[HASH][0]",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we continue to show the same output in plan? Rather than the hash? Two reasons:

  1. The plan can be serialized to a file and be consumed again.
  2. Users should be able to filter out and see the exact diff.

Can you also confirm that after these changes a plan serialized and used with a --plan flag continues to work?

@shreyas-goenka shreyas-goenka Aug 4, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For me ensuring (1) is more important. (2) is optional and maybe compacting the plan is actually better for readibility.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We won't get the diff if we want to hash since there is no way to retrieve the old dashboard state from the hash anyways

"old": "{\"pages\":[{\"name\":\"02724bf2\",\"displayName\":\"Dashboard test bundle-deploy-dashboard\"}]}\n",
"new": "{\"pages\":[{\"name\":\"02724bf2\",\"displayName\":\"Dashboard test bundle-deploy-dashboard\"}]}\n",
"remote": "{\"pages\":[{\"displayName\":\"Dashboard test bundle-deploy-dashboard\",\"name\":\"02724bf2\",\"pageType\":\"PAGE_TYPE_CANVAS\"}]}\n"
"old": "sha256_hashed_in_state:[HASH][0]",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

@denik

denik commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

This is a high risk change for serialized plan, so please convert dashboard tests to do EnvMatrix.READPLAN variant (ideally as a separate PR so we can confirm it all works on main).

@Sankalp-Mittal Sankalp-Mittal changed the title direct: store serialized_dashboard in state as a content hash direct: store a dashboard contents digest in state instead of the contents Aug 5, 2026
janniklasrose pushed a commit to GrantIsEaton/cli that referenced this pull request Aug 5, 2026
## Changes

Add `EnvMatrix.READPLAN = ["", "1"]` to the dashboard acceptance tests
that
deploy, and route their `bundle deploy` calls through the `readplanarg`
helper so
each test runs twice: once with an in-memory plan, once applying a plan
saved to
a file with `--plan`.

Because all `EnvMatrix` variants must produce identical output files,
any
divergence between the two deploy paths now fails the test
automatically.

Tests converted:

| Test | What it exercises |
| --- | --- |
| `resources/dashboards/change-serialized-dashboard` | edits
`serialized_dashboard` → update; asserts the PATCH/POST request bodies |
| `resources/dashboards/nested-folders` | reads `serialized_dashboard`
back from the API after deploy |
| `resources/dashboards/publish-failure-cleans-up-dashboard` | publish
failure path and the cleanup DELETE |
| `deployment/bind/dashboard` (+ `recreation/`) | bind writes state
without going through `DoCreate`/`DoUpdate` |

## Why

`bundle deploy --plan` takes a different code path than a plain deploy:
everything applied comes from the serialized plan file rather than from
a plan
computed in memory. Only one dashboard test exercised that path, so a
regression
in what a saved plan carries for dashboards would have gone uncaught.

This lands the coverage on its own, ahead of the `serialized_dashboard`
content-hash change (databricks#6105), so that any later failure is unambiguously
attributable to that change rather than to a pre-existing gap.

## Result

No request or state golden changed. The only golden edits are removed
`>>> [CLI] bundle deploy` trace lines — the deploys are no longer traced
because
the command line itself differs between variants (`--plan` vs none).

That the shared goldens are byte-identical across both variants is the
point:
`out.patch.requests.direct.txt`, `out.post.requests.txt` and
`out.state_after_bind.*.json` confirm both paths issue the same API
requests and
persist the same state on `main` today.

## Not converted

`migrate/dashboards` is deliberately left alone. It asserts on GET
requests to
`//dashboards` to confirm the direct engine ran, but a saved-plan deploy
skips
the read phase, so no GETs are recorded and that assertion has nothing
to match.
Converting it would mean weakening a real assertion. Migration therefore
remains
uncovered for the saved-plan path — worth noting, since like bind it
writes state
without going through `DoCreate`/`DoUpdate`.

## Tests

Acceptance-only; no production code changes. Verified green on
unmodified
`origin/main` before converting, so failures were attributable. `task
fmt`,
`task ws` and `task lint-q` are clean.

This pull request and its description were written by Isaac.
@Sankalp-Mittal
Sankalp-Mittal force-pushed the sankalp-mittal/dashboards-sha-state branch from 7b5f9c7 to e3dd4f3 Compare August 6, 2026 12:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants